/*
Copyright 2019 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package repository

import (
	"fmt"
	"strings"

	"github.com/pkg/errors"
	admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
	admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1"
	rbacv1 "k8s.io/api/rbac/v1"
	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
	apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"

	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
	clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3"
	"sigs.k8s.io/cluster-api/cmd/clusterctl/client/config"
	yaml "sigs.k8s.io/cluster-api/cmd/clusterctl/client/yamlprocessor"
	"sigs.k8s.io/cluster-api/cmd/clusterctl/internal/scheme"
	"sigs.k8s.io/cluster-api/cmd/clusterctl/internal/util"
	utilyaml "sigs.k8s.io/cluster-api/util/yaml"
)

const (
	namespaceKind                      = "Namespace"
	clusterRoleKind                    = "ClusterRole"
	clusterRoleBindingKind             = "ClusterRoleBinding"
	roleBindingKind                    = "RoleBinding"
	certificateKind                    = "Certificate"
	mutatingWebhookConfigurationKind   = "MutatingWebhookConfiguration"
	validatingWebhookConfigurationKind = "ValidatingWebhookConfiguration"
	customResourceDefinitionKind       = "CustomResourceDefinition"
)

// Components wraps a YAML file that defines the provider components
// to be installed in a management cluster (CRD, Controller, RBAC etc.)
// It is important to notice that clusterctl applies a set of processing steps to the “raw” component YAML read
// from the provider repositories:
// 1. Checks for all the variables in the component YAML file and replace with corresponding config values
// 2. Ensure all the provider components are deployed in the target namespace (apply only to namespaced objects)
// 3. Ensure all the ClusterRoleBinding which are referencing namespaced objects have the name prefixed with the namespace name
// 4. Adds labels to all the components in order to allow easy identification of the provider objects.
type Components interface {
	// Provider holds configuration of the provider the provider components belong to.
	config.Provider

	// Version of the provider.
	Version() string

	// Variables required by the provider components.
	// This value is derived by the component YAML.
	Variables() []string

	// Images required to install the provider components.
	// This value is derived by the component YAML.
	Images() []string

	// TargetNamespace where the provider components will be installed.
	// By default this value is derived by the component YAML, but it is possible to override it
	// during the creation of the Components object.
	TargetNamespace() string

	// InventoryObject returns the clusterctl inventory object representing the provider that will be
	// generated by this components.
	InventoryObject() clusterctlv1.Provider

	// Yaml return the provider components in the form of a YAML file.
	Yaml() ([]byte, error)

	// Objs return the components in the form of a list of Unstructured objects.
	Objs() []unstructured.Unstructured
}

// components implement Components.
type components struct {
	config.Provider
	version         string
	variables       []string
	images          []string
	targetNamespace string
	objs            []unstructured.Unstructured
}

// ensure components implement Components.
var _ Components = &components{}

func (c *components) Version() string {
	return c.version
}

func (c *components) Variables() []string {
	return c.variables
}

func (c *components) Images() []string {
	return c.images
}

func (c *components) TargetNamespace() string {
	return c.targetNamespace
}

func (c *components) InventoryObject() clusterctlv1.Provider {
	labels := getCommonLabels(c.Provider)
	labels[clusterctlv1.ClusterctlCoreLabelName] = clusterctlv1.ClusterctlCoreLabelInventoryValue

	return clusterctlv1.Provider{
		TypeMeta: metav1.TypeMeta{
			APIVersion: clusterctlv1.GroupVersion.String(),
			Kind:       "Provider",
		},
		ObjectMeta: metav1.ObjectMeta{
			Namespace: c.targetNamespace,
			Name:      c.ManifestLabel(),
			Labels:    labels,
		},
		ProviderName: c.Name(),
		Type:         string(c.Type()),
		Version:      c.version,
	}
}

func (c *components) Objs() []unstructured.Unstructured {
	return c.objs
}

func (c *components) Yaml() ([]byte, error) {
	return utilyaml.FromUnstructured(c.objs)
}

// ComponentsAlterFn defines the function that is used to alter the components.Objs().
type ComponentsAlterFn func(objs []unstructured.Unstructured) ([]unstructured.Unstructured, error)

// AlterComponents provides a mechanism to alter the component.Objs from outside
// the repository module.
func AlterComponents(comps Components, alterFn ComponentsAlterFn) error {
	c, ok := comps.(*components)
	if !ok {
		return errors.New("could not alter components as Components is not of the correct type")
	}

	alteredObjs, err := alterFn(c.Objs())
	if err != nil {
		return err
	}
	c.objs = alteredObjs
	return nil
}

// ComponentsOptions represents specific inputs that are passed in to
// clusterctl library. These are user specified inputs.
type ComponentsOptions struct {
	Version         string
	TargetNamespace string
	// SkipTemplateProcess allows for skipping the call to the template processor, including also variable replacement in the component YAML.
	// NOTE this works only if the rawYaml is a valid yaml by itself, like e.g when using envsubst/the simple processor.
	SkipTemplateProcess bool
}

// ComponentsInput represents all the inputs required by NewComponents.
type ComponentsInput struct {
	Provider     config.Provider
	ConfigClient config.Client
	Processor    yaml.Processor
	RawYaml      []byte
	Options      ComponentsOptions
}

// NewComponents returns a new objects embedding a component YAML file
//
// It is important to notice that clusterctl applies a set of processing steps to the “raw” component YAML read
// from the provider repositories:
// 1. Checks for all the variables in the component YAML file and replace with corresponding config values
// 2. The variables replacement can be skipped using the SkipTemplateProcess flag in the input options
// 3. Ensure all the provider components are deployed in the target namespace (apply only to namespaced objects)
// 4. Ensure all the ClusterRoleBinding which are referencing namespaced objects have the name prefixed with the namespace name
// 5. Adds labels to all the components in order to allow easy identification of the provider objects.
func NewComponents(input ComponentsInput) (Components, error) {
	variables, err := input.Processor.GetVariables(input.RawYaml)
	if err != nil {
		return nil, err
	}

	// If requested, we are skipping the call to the template processor; however, it is important to
	// notice that this could work only if the rawYaml is a valid yaml by itself.
	processedYaml := input.RawYaml
	if !input.Options.SkipTemplateProcess {
		processedYaml, err = input.Processor.Process(input.RawYaml, input.ConfigClient.Variables().Get)
		if err != nil {
			return nil, errors.Wrap(err, "failed to perform variable substitution")
		}
	}

	// Transform the yaml in a list of objects, so following transformation can work on typed objects (instead of working on a string/slice of bytes)
	objs, err := utilyaml.ToUnstructured(processedYaml)
	if err != nil {
		return nil, errors.Wrap(err, "failed to parse yaml")
	}

	// Apply image overrides, if defined
	objs, err = util.FixImages(objs, func(image string) (string, error) {
		return input.ConfigClient.ImageMeta().AlterImage(input.Provider.ManifestLabel(), image)
	})
	if err != nil {
		return nil, errors.Wrap(err, "failed to apply image overrides")
	}

	// Inspect the list of objects for the images required by the provider component.
	images, err := util.InspectImages(objs)
	if err != nil {
		return nil, errors.Wrap(err, "failed to detect required images")
	}

	// inspect the list of objects for the default target namespace
	// the default target namespace is the namespace object defined in the component yaml read from the repository, if any
	defaultTargetNamespace, err := inspectTargetNamespace(objs)
	if err != nil {
		return nil, errors.Wrap(err, "failed to detect default target namespace")
	}

	// Ensures all the provider components are deployed in the target namespace (apply only to namespaced objects)
	// if targetNamespace is not specified, then defaultTargetNamespace is used. In case both targetNamespace and defaultTargetNamespace
	// are empty, an error is returned

	if input.Options.TargetNamespace == "" {
		input.Options.TargetNamespace = defaultTargetNamespace
	}

	if input.Options.TargetNamespace == "" {
		return nil, errors.New("target namespace can't be defaulted. Please specify a target namespace")
	}

	// add a Namespace object if missing (ensure the targetNamespace will be created)
	objs = addNamespaceIfMissing(objs, input.Options.TargetNamespace)

	// fix Namespace name in all the objects
	objs, err = fixTargetNamespace(objs, input.Options.TargetNamespace)
	if err != nil {
		return nil, errors.Wrap(err, "failed to set the TargetNamespace on the components")
	}

	// ensures all the ClusterRole and ClusterRoleBinding have the name prefixed with the namespace name and that
	// all the clusterRole/clusterRoleBinding namespaced subjects refers to targetNamespace
	// Nb. Making all the RBAC rules "namespaced" is required for supporting multi-tenancy
	objs, err = fixRBAC(objs, input.Options.TargetNamespace)
	if err != nil {
		return nil, errors.Wrap(err, "failed to fix ClusterRoleBinding names")
	}

	// Add common labels.
	objs = addCommonLabels(objs, input.Provider)

	return &components{
		Provider:        input.Provider,
		version:         input.Options.Version,
		variables:       variables,
		images:          images,
		targetNamespace: input.Options.TargetNamespace,
		objs:            objs,
	}, nil
}

// inspectTargetNamespace identifies the name of the namespace object contained in the components YAML, if any.
// In case more than one Namespace object is identified, an error is returned.
func inspectTargetNamespace(objs []unstructured.Unstructured) (string, error) {
	namespace := ""
	for _, o := range objs {
		// if the object has Kind Namespace
		if o.GetKind() == namespaceKind {
			// grab the name (or error if there is more than one Namespace object)
			if namespace != "" {
				return "", errors.New("Invalid manifest. There should be no more than one resource with Kind Namespace in the provider components yaml")
			}
			namespace = o.GetName()
		}
	}
	return namespace, nil
}

// addNamespaceIfMissing adda a Namespace object if missing (this ensure the targetNamespace will be created).
func addNamespaceIfMissing(objs []unstructured.Unstructured, targetNamespace string) []unstructured.Unstructured {
	namespaceObjectFound := false
	for _, o := range objs {
		// if the object has Kind Namespace, fix the namespace name
		if o.GetKind() == namespaceKind {
			namespaceObjectFound = true
		}
	}

	// if there isn't an object with Kind Namespace, add it
	if !namespaceObjectFound {
		objs = append(objs, unstructured.Unstructured{
			Object: map[string]interface{}{
				"kind": namespaceKind,
				"metadata": map[string]interface{}{
					"name": targetNamespace,
				},
			},
		})
	}

	return objs
}

// fixTargetNamespace ensures all the provider components are deployed in the target namespace (apply only to namespaced objects).
func fixTargetNamespace(objs []unstructured.Unstructured, targetNamespace string) ([]unstructured.Unstructured, error) {
	for i := range objs {
		o := objs[i]

		// if the object has Kind Namespace, fix the namespace name
		if o.GetKind() == namespaceKind {
			o.SetName(targetNamespace)
		}

		originalNamespace := o.GetNamespace()

		// if the object is namespaced, set the namespace name
		if util.IsResourceNamespaced(o.GetKind()) {
			o.SetNamespace(targetNamespace)
		}

		if o.GetKind() == mutatingWebhookConfigurationKind || o.GetKind() == validatingWebhookConfigurationKind || o.GetKind() == customResourceDefinitionKind {
			var err error
			o, err = fixWebhookNamespaceReferences(o, targetNamespace)
			if err != nil {
				return nil, err
			}
		}

		if o.GetKind() == certificateKind {
			var err error
			o, err = fixCertificate(o, originalNamespace, targetNamespace)
			if err != nil {
				return nil, err
			}
		}
		objs[i] = o
	}
	return objs, nil
}

func fixWebhookNamespaceReferences(o unstructured.Unstructured, targetNamespace string) (unstructured.Unstructured, error) {
	annotations := o.GetAnnotations()
	secretNamespacedName, ok := annotations["cert-manager.io/inject-ca-from"]
	if ok {
		secretNameSplit := strings.Split(secretNamespacedName, "/")
		if len(secretNameSplit) != 2 {
			return o, fmt.Errorf("object %s %s does not have a correct value for cert-manager.io/inject-ca-from", o.GetKind(), o.GetName())
		}
		annotations["cert-manager.io/inject-ca-from"] = targetNamespace + "/" + secretNameSplit[1]
		o.SetAnnotations(annotations)
	}

	switch o.GetKind() {
	case mutatingWebhookConfigurationKind:
		return fixMutatingWebhookNamespaceReferences(o, targetNamespace)

	case validatingWebhookConfigurationKind:
		return fixValidatingWebhookNamespaceReferences(o, targetNamespace)

	case customResourceDefinitionKind:
		return fixCRDWebhookNamespaceReference(o, targetNamespace)
	}

	return o, errors.Errorf("failed to patch %s %s version", o.GroupVersionKind().Version, o.GetKind())
}

func fixMutatingWebhookNamespaceReferences(o unstructured.Unstructured, targetNamespace string) (unstructured.Unstructured, error) {
	version := o.GroupVersionKind().Version
	switch version {
	case admissionregistrationv1beta1.SchemeGroupVersion.Version:
		b := &admissionregistrationv1beta1.MutatingWebhookConfiguration{}
		if err := scheme.Scheme.Convert(&o, b, nil); err != nil {
			return o, err
		}
		for _, w := range b.Webhooks {
			if w.ClientConfig.Service != nil {
				w.ClientConfig.Service.Namespace = targetNamespace
			}
		}
		return o, scheme.Scheme.Convert(b, &o, nil)
	case admissionregistrationv1.SchemeGroupVersion.Version:
		b := &admissionregistrationv1.MutatingWebhookConfiguration{}
		if err := scheme.Scheme.Convert(&o, b, nil); err != nil {
			return o, err
		}
		for _, w := range b.Webhooks {
			if w.ClientConfig.Service != nil {
				w.ClientConfig.Service.Namespace = targetNamespace
			}
		}
		return o, scheme.Scheme.Convert(b, &o, nil)
	}
	return o, errors.Errorf("failed to patch %s MutatingWebhookConfiguration", version)
}

func fixValidatingWebhookNamespaceReferences(o unstructured.Unstructured, targetNamespace string) (unstructured.Unstructured, error) {
	version := o.GroupVersionKind().Version
	switch version {
	case admissionregistrationv1beta1.SchemeGroupVersion.Version:
		b := &admissionregistrationv1beta1.ValidatingWebhookConfiguration{}
		if err := scheme.Scheme.Convert(&o, b, nil); err != nil {
			return o, err
		}
		for _, w := range b.Webhooks {
			if w.ClientConfig.Service != nil {
				w.ClientConfig.Service.Namespace = targetNamespace
			}
		}
		return o, scheme.Scheme.Convert(b, &o, nil)
	case admissionregistrationv1.SchemeGroupVersion.Version:
		b := &admissionregistrationv1.ValidatingWebhookConfiguration{}
		if err := scheme.Scheme.Convert(&o, b, nil); err != nil {
			return o, err
		}
		for _, w := range b.Webhooks {
			if w.ClientConfig.Service != nil {
				w.ClientConfig.Service.Namespace = targetNamespace
			}
		}
		return o, scheme.Scheme.Convert(b, &o, nil)
	}
	return o, errors.Errorf("failed to patch %s ValidatingWebhookConfiguration", version)
}

func fixCRDWebhookNamespaceReference(o unstructured.Unstructured, targetNamespace string) (unstructured.Unstructured, error) {
	version := o.GroupVersionKind().Version
	switch version {
	case apiextensionsv1beta1.SchemeGroupVersion.Version:
		crd := &apiextensionsv1beta1.CustomResourceDefinition{}
		if err := scheme.Scheme.Convert(&o, crd, nil); err != nil {
			return o, err
		}
		if crd.Spec.Conversion != nil && crd.Spec.Conversion.WebhookClientConfig != nil && crd.Spec.Conversion.WebhookClientConfig.Service != nil {
			crd.Spec.Conversion.WebhookClientConfig.Service.Namespace = targetNamespace
		}
		return o, scheme.Scheme.Convert(crd, &o, nil)

	case apiextensionsv1.SchemeGroupVersion.Version:
		crd := &apiextensionsv1.CustomResourceDefinition{}
		if err := scheme.Scheme.Convert(&o, crd, nil); err != nil {
			return o, err
		}
		if crd.Spec.Conversion != nil && crd.Spec.Conversion.Webhook != nil && crd.Spec.Conversion.Webhook.ClientConfig != nil && crd.Spec.Conversion.Webhook.ClientConfig.Service != nil {
			crd.Spec.Conversion.Webhook.ClientConfig.Service.Namespace = targetNamespace
		}
		return o, scheme.Scheme.Convert(crd, &o, nil)
	}
	return o, errors.Errorf("failed to patch %s CustomResourceDefinition", version)
}

// fixCertificate fixes the dnsNames of cert-manager Certificates. The DNS names contain the dns names of the provider
// services (including the namespace) and thus have to be modified to use the target namespace instead.
func fixCertificate(o unstructured.Unstructured, originalNamespace, targetNamespace string) (unstructured.Unstructured, error) {
	dnsNames, ok, err := unstructured.NestedStringSlice(o.UnstructuredContent(), "spec", "dnsNames")
	if err != nil {
		return o, errors.Wrapf(err, "failed to get .spec.dnsNames from Certificate %s/%s", o.GetNamespace(), o.GetName())
	}
	// Return if we don't find .spec.dnsNames.
	if !ok {
		return o, nil
	}

	// Iterate through dnsNames and adjust the namespace.
	// The dnsNames slice usually looks like this:
	// - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc
	// - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc.cluster.local
	for i, dnsName := range dnsNames {
		dnsNames[i] = strings.Replace(dnsName, fmt.Sprintf(".%s.", originalNamespace), fmt.Sprintf(".%s.", targetNamespace), 1)
	}

	if err := unstructured.SetNestedStringSlice(o.UnstructuredContent(), dnsNames, "spec", "dnsNames"); err != nil {
		return o, errors.Wrapf(err, "failed to set .spec.dnsNames to Certificate %s/%s", o.GetNamespace(), o.GetName())
	}

	return o, nil
}

// fixRBAC ensures all the ClusterRole and ClusterRoleBinding have the name prefixed with the namespace name and that
// all the clusterRole/clusterRoleBinding namespaced subjects refers to targetNamespace.
func fixRBAC(objs []unstructured.Unstructured, targetNamespace string) ([]unstructured.Unstructured, error) {
	renamedClusterRoles := map[string]string{}
	for _, o := range objs {
		// if the object has Kind ClusterRole
		if o.GetKind() == clusterRoleKind {
			// assign a namespaced name
			currentName := o.GetName()
			newName := fmt.Sprintf("%s-%s", targetNamespace, currentName)
			o.SetName(newName)

			renamedClusterRoles[currentName] = newName
		}
	}

	for i := range objs {
		o := objs[i]
		switch o.GetKind() {
		case clusterRoleBindingKind: // if the object has Kind ClusterRoleBinding
			// Convert Unstructured into a typed object
			b := &rbacv1.ClusterRoleBinding{}
			if err := scheme.Scheme.Convert(&o, b, nil); err != nil {
				return nil, err
			}

			// assign a namespaced name
			b.Name = fmt.Sprintf("%s-%s", targetNamespace, b.Name)

			// ensure that namespaced subjects refers to targetNamespace
			for k := range b.Subjects {
				if b.Subjects[k].Namespace != "" {
					b.Subjects[k].Namespace = targetNamespace
				}
			}

			// if the referenced ClusterRole was renamed, change the RoleRef
			if newName, ok := renamedClusterRoles[b.RoleRef.Name]; ok {
				b.RoleRef.Name = newName
			}

			// Convert ClusterRoleBinding back to Unstructured
			if err := scheme.Scheme.Convert(b, &o, nil); err != nil {
				return nil, err
			}
			objs[i] = o

		case roleBindingKind: // if the object has Kind RoleBinding
			// Convert Unstructured into a typed object
			b := &rbacv1.RoleBinding{}
			if err := scheme.Scheme.Convert(&o, b, nil); err != nil {
				return nil, err
			}

			// ensure that namespaced subjects refers to targetNamespace
			for k := range b.Subjects {
				if b.Subjects[k].Namespace != "" {
					b.Subjects[k].Namespace = targetNamespace
				}
			}

			// Convert RoleBinding back to Unstructured
			if err := scheme.Scheme.Convert(b, &o, nil); err != nil {
				return nil, err
			}
			objs[i] = o
		}
	}

	return objs, nil
}

// addCommonLabels ensures all the provider components have a consistent set of labels.
func addCommonLabels(objs []unstructured.Unstructured, provider config.Provider) []unstructured.Unstructured {
	for _, o := range objs {
		labels := o.GetLabels()
		if labels == nil {
			labels = map[string]string{}
		}
		for k, v := range getCommonLabels(provider) {
			labels[k] = v
		}
		o.SetLabels(labels)
	}

	return objs
}

func getCommonLabels(provider config.Provider) map[string]string {
	return map[string]string{
		clusterctlv1.ClusterctlLabelName: "",
		clusterv1.ProviderLabelName:      provider.ManifestLabel(),
	}
}
